﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NexTrackDesktop.Media;
using System.Windows;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Microsoft.Win32;
using System.Threading;
using System.IO;
using System.Windows.Threading;

namespace NexTrackDesktop.ViewModels
{
    /// <summary>
    /// This class handles events for the MediaLibrary view
    /// </summary>
    public class MediaLibraryViewModel : ViewModelBase
    {
        private Dispatcher Dispatcher;

        //Small data structure using properties for proper binding.
        public class Artist : ViewModelBase
        {
            public string Name { get; set; }
            public int NumberAlbums { get; set; }
            public int NumberTracks { get; set; }

            private bool _selected;
            public bool Selected
            {
                get { return _selected; }
                set 
                { 
                    _selected = value; 
                    NotifyPropertyChanged(() => Selected);
                    
                    if(ArtistSelectionChanged != null) ArtistSelectionChanged();
                }
            }

            public event Action ArtistSelectionChanged;

        }

        public class Album : ViewModelBase
        {
            public string Title { get; set; }
            public int NumberTracks { get; set; }
            public string AlbumArtist { get; set; }

            private bool _selected;
            public bool Selected
            {
                get { return _selected; }
                set 
                { 
                    _selected = value; 
                    NotifyPropertyChanged(() => Selected);
                    if(AlbumSelectionChanged != null) AlbumSelectionChanged();
                }
            }

            public event Action AlbumSelectionChanged;
        }

        public ObservableCollection<SongViewModel> Songs { get; private set; }
        public ObservableCollection<Artist> Artists { get; private set; }
        public ObservableCollection<Album> Albums { get; private set; }

        public ICommand AddMediaCommand { get; private set; }
        public ICommand AddFolderCommand { get; private set; }
        public ICommand AddToPlaylistCommand { get; private set; }

        public IEnumerable<SongViewModel> SelectedSongs
        {
            get { return Songs.Where(s => s.Selected); }
        }

        public IEnumerable<Artist> SelectedArtists
        {
            get { return Artists.Where(s => s.Selected); }
        }

        public IEnumerable<Album> SelectedAlbums
        {
            get { return Albums.Where(s => s.Selected); }
        }

        public MediaLibraryViewModel()
        {
            Dispatcher = Dispatcher.CurrentDispatcher;
            Client.MediaLibrary.SongAdded += new Action<Song>(library_SongAdded);

            //Add any existing songs to the media library view
            var songViewModels = Client.MediaLibrary.Items.Select(s => new SongViewModel(s));
            Songs = new ObservableCollection<SongViewModel>(songViewModels);

            resetView();

            createCommands();
        }

        Thread updateThread;
        DateTime nextUpdate;

        void pooledUpdate()
        {
            while (true)
            {
                Thread.Sleep(100);

                lock (this)
                {
                    if (DateTime.Now >= nextUpdate)
                        break;
                }
            }

            //using Dispatcher.Invoke will update the UI on the UI Thread.
            Dispatcher.Invoke((Action)(() =>
            {
                resetView();
            }));
        }

        /// <summary>
        /// This method adds the specified Song to the views oberservable collection
        /// </summary>
        /// <param name="song">Song to add to collection</param>
        void library_SongAdded(Song song)
        {
            //The Update Thread periodically checks nextUpdate to determine if
            //the view needs to be refreshed or not. Every time a song is added
            //we want to continue waiting. This way, when hundreds of songs are added
            //to the list, the view isn't updated repeatedly because this can
            //become expensive.
            lock (this)
            {
                nextUpdate = DateTime.Now + TimeSpan.FromSeconds(6);
            }

            //Once the update thread has updated the view, it terminates
            //and must be restarted
            if (updateThread == null || !updateThread.IsAlive)
            {
                updateThread = new Thread(new ThreadStart(pooledUpdate));
                updateThread.Start();
            }
        }

        /// <summary>
        /// This method links commands with user input for the view
        /// </summary>
        void createCommands()
        {
            AddMediaCommand = new DelegateCommand(addMedia, () => true);
            AddFolderCommand = new DelegateCommand(addFolder, () => true);
            AddToPlaylistCommand = new DelegateCommand(addToPlaylist,
                () => Client.Party != null && SelectedSongs.Count() > 0);
        }

        /// <summary>
        /// This method handles adding media to library through file dialog
        /// </summary>
        void addMedia()
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Filter = "Music Files|*.mp3; *.mp4; *.m4a; *.ogg; *.flac; *.cda; *.wma|All Files|*.*";
            ofd.CheckFileExists = true;
            ofd.CheckPathExists = true;
            ofd.Multiselect = true;
            ofd.InitialDirectory = "shell:My Music";

            if (ofd.ShowDialog() == true)
            {
                foreach (string file in ofd.FileNames)
                {
                    Client.MediaLibrary.AddFile(new Uri(file));
                }
            }

            Client.MediaLibrary.SaveLibrary();
        }

        /// <summary>
        /// This method handles adding media to library through folder dialog
        /// </summary>
        void addFolder()
        {
            System.Windows.Forms.FolderBrowserDialog fbd = new System.Windows.Forms.FolderBrowserDialog();
            //fbd.RootFolder = Environment.SpecialFolder.MyComputer;
            fbd.ShowNewFolderButton = false;

            if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                //The recursive process of adding files should run in the background.
                Action<object> threadStart = (o) =>
                {
                    Client.MediaLibrary.addFolderRecursive(o as string);
                    Client.MediaLibrary.SaveLibrary();
                };
                Thread t = new Thread(new ParameterizedThreadStart(threadStart));
                t.Start(fbd.SelectedPath);
            }
        }

        /// <summary>
        /// This method adds selected songs to playlist
        /// </summary>
        void addToPlaylist()
        {
            Client.Party.Playlist.AddSongs(SelectedSongs.Where(s => s.Selected).Select(s => s.Song));
        }

        /// <summary>
        /// Re-filters the various viewing windows.
        /// </summary>
        void resetView()
        {
            var libraryItems = Client.MediaLibrary.Items;

            //Selects a list of all albums and the number of tracks in each
            var albums = libraryItems
                .GroupBy(s => new { Album = s.Album, AlbumArtist = s.Artist } )
                .Select(a => new Album() 
                { 
                    Title = a.Key.Album, 
                    AlbumArtist = a.Key.AlbumArtist, 
                    NumberTracks = a.Count(),
                });

            //Selects a list of all artists, with a number of albums and number of tracks for each
            var artists = albums
                .GroupBy(a => a.AlbumArtist)
                .Select(a => new Artist()
                {
                    Name = a.Key,
                    NumberAlbums = a.Count(),
                    NumberTracks = a.Aggregate(0, (sub, aa) => sub + aa.NumberTracks)
                });

            //We use Songs.UpdateInPlace to update the existing viewmodels
            Songs.UpdateInPlace(libraryItems,
                (sv, i) => { sv.Song = i; sv.Selected = false; });

            //Artists and Albums don't have the proxy-style viewmodel that songs does so new collections are created
            Artists = new ObservableCollection<Artist>(artists);
            Albums = new ObservableCollection<Album>(albums);

            //Each entry in the artist and album list needs to notify the medialibrary of this change.
            //note: this could be done as a static event.
            foreach (Artist artist in Artists)
            {
                artist.ArtistSelectionChanged += ApplyArtistFilter;
            }

            foreach (Album album in Albums)
            {
                album.AlbumSelectionChanged += ApplyAlbumFilter;
            }

            //Update theview with Artists and Albums
            NotifyPropertyChanged(() => Artists);
            NotifyPropertyChanged(() => Albums);
        }

        private void ApplyArtistFilter()
        {
            var libraryItems = Client.MediaLibrary.Items;

            //Selects a list of all albums and the number of tracks in each
            var albums = libraryItems
                .GroupBy(s => new { Album = s.Album, AlbumArtist = s.Artist })
                .Select(a => new Album()
                {
                    Title = a.Key.Album,
                    AlbumArtist = a.Key.AlbumArtist,
                    NumberTracks = a.Count(),
                });

            //A set has quick .contains, so filter actions will be quick
            var filter = new HashSet<string>(SelectedArtists.Select(a => a.Name));

            var filteredAlbums = albums.Where(a => filter.Contains(a.AlbumArtist));
            var filteredSongs = libraryItems.Where(s => filter.Contains(s.Artist));

            Albums = new ObservableCollection<Album>(filteredAlbums);
            Songs.UpdateInPlace(filteredSongs.ToList(),
                (sv, i) => { sv.Song = i; sv.Selected = false; });

            foreach (Album album in Albums)
            {
                album.AlbumSelectionChanged += ApplyAlbumFilter;
            }

            NotifyPropertyChanged(() => Albums);
        }

        private void ApplyAlbumFilter()
        {
            var libraryItems = Client.MediaLibrary.Items;

            //Use string.format as a hack to use a HashSet<string> for filtering based on both album title and album artist.
            Func<string,string,string> filterKey = 
                (string albumTitle, string albumArtist) => string.Format("##{0}##{1}$", albumTitle, albumArtist);

            var filter = new HashSet<string>(SelectedAlbums.Select(a => filterKey(a.Title, a.AlbumArtist)));
            var filteredSongs = libraryItems.Where(s => filter.Contains(filterKey(s.Album, s.Artist)));
            
            Songs.UpdateInPlace(filteredSongs.ToList(),
                (sv, i) => { sv.Song = i; sv.Selected = false; });
        }
    }
}
